/**
 * 
 * NOTICE -- YOU MUST LEAVE THIS NOTICE IN PLACE.  PER THE TERMS OF THE LICENSE BELOW, YOU MAY SUBLICENSE THIS SOFTWARE 
 * IN ANY WAY THAT DOES NOT CONFLICT WITH THIS LICENSE.
 *
 * Copyright (c) 2011, Paul Gribben <paul.gribben@gmail.com>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *	
 *	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
 *	INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 *	DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 *	SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 *	WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 *	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
package org.softwise.rss2kindle.impl;

import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.AbstractMap.SimpleEntry;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.ccil.cowan.tagsoup.Parser;
import org.softwise.rss2kindle.model.EntryWriter;
import org.softwise.rss2kindle.model.RssEntryTransformer.RssEntry;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

public class HtmlLinkEntryWriter implements EntryWriter {
	private final XPath xpath = XPathFactory.newInstance().newXPath();
	private final String xpathFilter;
	private final XMLReader reader = new org.ccil.cowan.tagsoup.Parser();
	private final Transformer transformer;
	private final ElementConverter[] elementConverters;
	private final Map<String,String> styles;
	private final boolean headingRepeated;
	
	public HtmlLinkEntryWriter(final Transformer transformer,
			final String xpathFilter, ElementConverter[] elementConverters,
			final Map<String,String> styles, final boolean headingRepeated)
			throws Exception {
		reader.setFeature(Parser.namespacesFeature, false);
		reader.setFeature(Parser.namespacePrefixesFeature, false);

		this.xpathFilter = xpathFilter;
		this.transformer = transformer;
		this.styles = styles;
		this.headingRepeated = headingRepeated;
		this.elementConverters = elementConverters != null ? elementConverters
				: new ElementConverter[0];
	}

	public static class ElementConverter {
		private final ElemChecker elemChecker;
		private final ElemDef[] newElemDef;

		public ElementConverter(ElemChecker elemChecker, ElemDef[] newElemDef) {
			this.elemChecker = elemChecker;
			this.newElemDef = newElemDef;
		}

		public interface ElemChecker {
			boolean matches(Element testElem);
		}
		
		public static class ElemDef {
			private String name;
			Set<Map.Entry<String, String>> attributes = new HashSet<Map.Entry<String, String>>();
			
			public ElemDef(String name) {
				this.name = name;
			}
			public ElemDef(String name, String attrName, String attrValue) {
				this.name = name;
				attributes.add(new SimpleEntry<String,String>(attrName, attrValue));
			}
			public String getName() {
				return name;
			}
			public Set<Map.Entry<String, String>> getAttributes() {
				return attributes;
			}
		}
		/**
		 * 
		 * @param fromElem
		 * @return new element or null if not transformed
		 */
		public Element convert(Element fromElem, Document doc) throws Exception {
			if (elemChecker.matches(fromElem)) {
				NodeList childNodes = fromElem.getChildNodes();
				ElemDef elemDef = newElemDef[0];
				Element newElement = doc.createElement(elemDef.getName());
				if (elemDef.getAttributes() != null) {
					for (Map.Entry<String,String> attr : elemDef.getAttributes()) {
						newElement.setAttribute(attr.getKey(), attr.getValue());
					}
				}
				//newElement.setTextContent(fromElem.getTextContent()); // copy text content
				
				for (int i = 0; i < childNodes.getLength(); i++) {
					Node currElem = childNodes.item(i);
					newElement.appendChild(doc.adoptNode(currElem));
				}
				return newElement;
			} else {
				return null;
			}
		}
	}

	@Override
	public void write(Writer writer, RssEntry entry) throws Exception {
		URL linkUrl = new URL(entry.getLink());
		Reader urlReader = new InputStreamReader(linkUrl.openStream(), "UTF-8");
		DOMResult result = new DOMResult();
		transformer.transform(
				new SAXSource(reader, new InputSource(urlReader)), result);
		Node docNode = result.getNode();
		NodeList elements = (NodeList) xpath.evaluate(xpathFilter, docNode,
				XPathConstants.NODESET);
		Document doc = DocumentBuilderFactory.newInstance()
				.newDocumentBuilder().newDocument();
		Element articleElement = doc.createElement("article");
		
		for (int i = 0; i < elements.getLength(); i++) {
			Element currElem = (Element)elements.item(i);
			// check if element can be converted
			for (int c=0; c<elementConverters.length; c++) {
				Element newCurrElem = elementConverters[c].convert(currElem, doc);
				if (newCurrElem != null) {
					currElem = newCurrElem;
					break;
				}
			}
			// add article element to new document
			articleElement.appendChild(doc.adoptNode(currElem));
		}
		
		// serialize XML to writer
		transformer.transform(new DOMSource(articleElement), new StreamResult(
				writer));
	}

	@Override
	public void writeHeaderInfo(Writer writer) throws Exception {
		if (styles != null && styles.size()>0) {
			writer.write("<style type=\"text/css\">");
			for (Map.Entry<String,String> style : styles.entrySet()) {
				writer.write(style.getKey() + " {" + style.getValue() + "} ");
			}
			writer.write("</style>");
		}
		
	}
	
	@Override
	public boolean isHeadingRepeated() {
		return headingRepeated;
	}
}
